﻿//-----------------------------------------------------------------------
// <copyright file="MethodUtility.cs">
//     Copyright (c) Nicole Calinoiu. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
using System;
using System.Linq;

using Microsoft.FxCop.Sdk;

namespace Bordecal.FxCop.Sdk
{
	/// <summary>
	/// Extension methods for the <see cref="Method"/> type.
	/// </summary>
	public static class MethodUtility
	{
		/// <summary>
		/// Determines whether the body of a method has a wrapping try/catch.
		/// </summary>
		/// <param name="method">The target method.</param>
		/// <param name="returnTrueForEmptyMethod">Indicates whether a <c>true</c> value should be returned for a method that does not have any "interesting" code, even if it does not have a wrapping try/catch.</param>
		/// <returns>
		/// True if the method body has a wrapping try/catch; otherwise, false.
		/// </returns>
		public static bool HasWrappingTryCatch(this Method method, bool returnTrueForEmptyMethod)
		{
			if (method == null)
			{
				throw new ArgumentNullException("method");
			}

			bool nontrivialInstructionsFound = false;
			bool instructionsFoundOutsideHandler = false;
			bool catchFound = false;
			int topLevelTryCount = 0;
			int currentHandlerLevel = 0;

			foreach (Instruction instruction in method.Instructions)
			{
				switch (instruction.OpCode)
				{
					case OpCode.Nop:
					case OpCode._Locals:
					case OpCode.Ret:
						break;

					case OpCode._Try:
						if (currentHandlerLevel == 0)
						{
							topLevelTryCount++;
						}

						currentHandlerLevel++;
						break;

					case OpCode._Finally:
					case OpCode._Filter:
						currentHandlerLevel++;
						break;

					case OpCode._Catch:
						if (currentHandlerLevel == 0)
						{
							catchFound = true;
						}
						
						currentHandlerLevel++;
						break;

					case OpCode._EndTry:
					case OpCode._EndHandler:
					case OpCode.Endfinally:
					case OpCode._EndFilter:
						currentHandlerLevel--;
						break;

					default:
						nontrivialInstructionsFound = true;
						if (currentHandlerLevel == 0)
						{
							instructionsFoundOutsideHandler = true;
						}

						break;
				}
			}

			bool result = true;
			if (nontrivialInstructionsFound || (!returnTrueForEmptyMethod))
			{
				if ((!catchFound) || instructionsFoundOutsideHandler || (topLevelTryCount > 1))
				{
					result = false;
				}
			}

			return result;
		}

		/// <summary>
		/// Determines whether a <see cref="Method"/> is an explicit implementation of an interface method.
		/// </summary>
		/// <param name="method">The target <see cref="Method"/>.</param>
		/// <param name="interfaceMethodName">Name of the interface method.</param>
		/// <returns>True if the method is an explicit implementation of the specified method; otherwise false.</returns>
		public static bool IsExplicitImplementationOf(this Method method, string interfaceMethodName)
		{
			if (method == null)
			{
				throw new ArgumentNullException("method");
			}

			bool result = false;

			MethodCollection implementedMethods = method.ImplementedInterfaceMethods;
			if (implementedMethods != null)
			{
				result = implementedMethods.Any(m => m.Name.Name.Equals(interfaceMethodName, StringComparison.Ordinal));
			}

			return result;
		}

		/// <summary>
		/// Gets a method overload with at least one additional parameter of given type.
		/// </summary>
		/// <param name="method">The target <see cref="Method"/>.</param>
		/// <param name="parameterType">Type of the extra parameter.</param>
		/// <returns>A method overload with an extra parameter of the specified type, if available.  Otherwise, returns <c>null</c>.</returns>
		public static Method GetOverloadWithExtraParameters(this Method method, TypeNode parameterType)
		{
			if (method == null)
			{
				throw new ArgumentNullException("method");
			}

			Method result = method.GetOverloadWithExtraParameters(parameterType, method.Name.Name);
			if (result == null)
			{
				MethodCollection implementedMethods = method.ImplementedInterfaceMethods;
				if (implementedMethods != null)
				{
					foreach (Method implementedMethod in implementedMethods)
					{
						if (implementedMethod.Name.Name != method.Name.Name)
						{
							result = method.GetOverloadWithExtraParameters(parameterType, implementedMethod.Name.Name);
							if (result != null)
							{
								break;
							}
						}
					}
				}
			}

			return result;
		}

		private static Method GetOverloadWithExtraParameters(this Method target, TypeNode parameterType, string methodName)
		{
			Method result = null;

			MemberCollection members = target.DeclaringType.Members;
			if (members.Count > 0)
			{
				int targetCount = MethodUtility.CountParameters(target, parameterType);

				foreach (Member member in members)
				{
					Method method = member as Method;
					if ((method != null) && (method != target) && (method.ObsoleteAttribute == null))
					{
						if ((method.Name.Name == methodName) || method.IsExplicitImplementationOf(methodName))
						{
							if ((method.Parameters.Count > target.Parameters.Count) || method.HasParameterArray())
							{
								if (MethodUtility.CountParameters(method, parameterType) > targetCount)
								{
									result = method;
									break;
								}
							}
						}
					}
				}
			}

			return result;
		}

		private static int CountParameters(Method target, TypeNode parameterType)
		{
			return (from p in target.Parameters
					where p.Type.IsAssignableTo(parameterType)
					select p).Count();
		}

		/// <summary>
		/// Determines whether a <see cref="Method"/> has an argument that is a parameter array.
		/// </summary>
		/// <param name="method">The target <see cref="Method"/>.</param>
		/// <returns>True if the method has a parameter array argument; otherwise, false.</returns>
		private static bool HasParameterArray(this Method method)
		{
			return (from p in method.Parameters
					where p.IsParameterArray()
					select p).Any();
		}
	}
}